﻿//---------------------------------------------------------------------------
// <copyright file="TwoLineLabel.cs" company="Microsoft Corporation">
//     Copyright (C) Microsoft Corporation.  All rights reserved.
// </copyright>
//---------------------------------------------------------------------------

namespace Microsoft.Windows.Controls.Ribbon
{
    #region Using declarations

    using System;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Media;
    using System.Windows.Shapes;
    
    #endregion

    /// <summary>
    ///   A specialized label that can lay itself out on at most two lines, and
    ///   also has the ability to display a special Glyph inline with its text.
    /// </summary>
    public class TwoLineLabel : FrameworkElement
    {
        #region Fields

        /// <summary>
        ///   The minimum defined width of a label.
        /// </summary>
        private const double LabelMinWidth = 44.0;

        /// <summary>
        ///   The maximum defined width of the label.
        /// </summary>
        private const double LabelMaxHeight = 30.0;

        /// <summary>
        ///   The Run used to wrap together the text and glyph.
        /// </summary>
        private Run _run;

        /// <summary>
        ///   The glyph appending the TwoLineLabel.
        /// </summary>
        private Path _path;
        
        /// <summary>
        ///   The TextBlock that encapsulates the Run and glyph.
        /// </summary>
        private TextBlock _textBlock;

        #endregion

        #region Public Properties

        /// <summary>
        ///   Gets or sets the TextWrapping applied to the internal text block.
        /// </summary>
        public TextWrapping TextWrapping
        {
            get { return (TextWrapping)GetValue(TextWrappingProperty); }
            set { SetValue(TextWrappingProperty, value); }
        }

        /// <summary>
        ///   Adds TwoLineLabel as an owner of this property.
        /// </summary>
        public static readonly DependencyProperty TextWrappingProperty =
            TextBlock.TextWrappingProperty.AddOwner(typeof(TwoLineLabel));

        /// <summary>
        ///   Gets or sets the TextAlignment set on the internal text block.
        /// </summary>
        public TextAlignment TextAlignment
        {
            get { return (TextAlignment)GetValue(TextAlignmentProperty); }
            set { SetValue(TextAlignmentProperty, value); }
        }

        /// <summary>
        ///   Adds TwoLineLabel as an owner of this property.
        /// </summary>
        public static readonly DependencyProperty TextAlignmentProperty =
            TextBlock.TextAlignmentProperty.AddOwner(typeof(TwoLineLabel));
        
        /// <summary>
        ///   Gets or sets the LineHeight set on the internal text block.
        /// </summary>
        [TypeConverter(typeof(LengthConverter))]
        public double LineHeight
        {
            get { return (double)GetValue(LineHeightProperty); }
            set { SetValue(LineHeightProperty, value); }
        }

        /// <summary>
        ///   Adds TwoLineLabel as an owner of this property.
        /// </summary>
        public static readonly DependencyProperty LineHeightProperty =
            TextBlock.LineHeightProperty.AddOwner(typeof(TwoLineLabel));
        
        /// <summary>
        ///   Gets or sets the LineStackingStrategy set on the internal text block.
        /// </summary>
        public LineStackingStrategy LineStackingStrategy
        {
            get { return (LineStackingStrategy)GetValue(LineStackingStrategyProperty); }
            set { SetValue(LineStackingStrategyProperty, value); }
        }

        /// <summary>
        ///   Adds TwoLineLabel as an owner of this property.
        /// </summary>
        public static readonly DependencyProperty LineStackingStrategyProperty =
            TextBlock.LineStackingStrategyProperty.AddOwner(typeof(TwoLineLabel));
        
        /// <summary>
        ///   Gets or sets the fill of the label's appending glyph.
        /// </summary>
        public Brush GlyphFill
        {
            get { return (Brush)GetValue(GlyphFillProperty); }
            set { SetValue(GlyphFillProperty, value); }
        }

        /// <summary>
        ///   Using a DependencyProperty as the backing store for GlyphFillProperty.  This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty GlyphFillProperty =
                    DependencyProperty.Register(
                            "GlyphFill",
                            typeof(Brush),
                            typeof(TwoLineLabel),
                            null);
        
        /// <summary>
        ///   Gets or sets the label's text.
        /// </summary>
        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        /// <summary>
        ///   Using a DependencyProperty as the backing store for TextProperty.  This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty TextProperty =
                    DependencyProperty.Register(
                            "Text",
                            typeof(string),
                            typeof(TwoLineLabel),
                            new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure));
        
        /// <summary>
        ///   Gets the Path data for this label's glyph.
        /// </summary>
        /// <param name="obj">The label whose Path data to get.</param>
        /// <returns>Returns the Path data for this label's glyph.</returns>
        public static Geometry GetData(DependencyObject obj)
        {
            return (Geometry)obj.GetValue(DataProperty);
        }

        /// <summary>
        ///   Sets the Path data for this label's glyph.
        /// </summary>
        /// <param name="obj">The label whose Path data to set.</param>
        /// <param name="value">The value to set.</param>
        public static void SetData(DependencyObject obj, Geometry value)
        {
            obj.SetValue(DataProperty, value);
        }

        /// <summary>
        ///   Defines Path data for the label's appending glyph.
        /// </summary>
        public static readonly DependencyProperty DataProperty =
                    DependencyProperty.RegisterAttached(
                            "Data",
                            typeof(Geometry),
                            typeof(TwoLineLabel),
                            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));
        
        /// <summary>
        ///   Gets or sets a value indicating whether the TwoLineLabel should be allowed to
        ///   layout on two lines or if it should be restricted to one.
        /// </summary>
        public bool HasTwoLines
        {
            get { return (bool)GetValue(HasTwoLinesProperty); }
            set { SetValue(HasTwoLinesProperty, value); }
        }

        /// <summary>
        ///   Using a DependencyProperty as the backing store for HasTwoLinesProperty.  This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty HasTwoLinesProperty = DependencyProperty.Register(
            "HasTwoLines",
            typeof(bool),
            typeof(TwoLineLabel),
            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));

        #endregion

        #region Protected Properties

        /// <summary>
        ///   Gets the number of visual children in the element.
        /// </summary>
        protected override int VisualChildrenCount
        {
            get { return _textBlock == null ? 0 : 1; }
        }

        #endregion

        #region Protected Methods

        /// <summary>
        ///   Measures the contents of the TwoLineLabel.  If TwoLineLabel.HasTwoLines is
        ///   true it attempts to lay out the contents on two lines, otherwise on one.  If
        ///   the associated glyph's path data is not null, it will be layed out inline
        ///   with the text.
        /// </summary>
        /// <param name="availableSize">
        ///   The available size for layout, this is ignored entirely for this special layout heuristic.
        /// </param>
        /// <returns>Returns the desired size of the element.</returns>
        protected override Size MeasureOverride(Size availableSize)
        {
            if (_textBlock == null)
            {
                this.CreateTextBlock();
            }

            Size infinity = new Size(double.PositiveInfinity, double.PositiveInfinity);

            _textBlock.ClearValue(TextBlock.WidthProperty);
            
            _run.Text = (string)Text;
            _path.Visibility = (_path.Data == null) ? Visibility.Collapsed : Visibility.Visible;

            // This measures the TextBlock with infinite bounds to see what its ideal
            // (one-line) Width would be.  If this width is greater than the minimum
            // width for the label and HasTwoLines is true, we try halving its site to
            // spread the label to two lines.
            _textBlock.Measure(infinity);
            _path.Measure(infinity);
            if (this.HasTwoLines)
            {
                double arrowWidth = Math.Max(1.0, _path.DesiredSize.Width);
                double width = _textBlock.DesiredSize.Width;
                double height = _textBlock.DesiredSize.Height;

                if (width > TwoLineLabel.LabelMinWidth)
                {
                    int i = 0;
                    do
                    {
                        // Set the TextBlock's width to half of its desired with to spread it
                        // to two lines, and re-run layout.
                        _textBlock.Width = Math.Max(LabelMinWidth, width / 2.0) + (arrowWidth * i++);
                        _textBlock.Measure(infinity);
                        _textBlock.Arrange(new Rect(_textBlock.DesiredSize));

                        // Expand the TextBlock's width to keep from clipping text when we
                        // naively resized to DesiredWidth / 2.
                        TextPointer tp = _textBlock.ContentStart.GetLineStartPosition(1);
                        if (tp != null)
                        {
                            Rect r = tp.GetCharacterRect(LogicalDirection.Backward);
                            Rect r2 = _textBlock.ContentEnd.GetCharacterRect(LogicalDirection.Backward);
                            _textBlock.Width = Math.Max(r.X, r2.X);
                            _textBlock.Measure(infinity);
                        }

                        // Continue to expand the width of the label while its height exceeds
                        // the allowable maximum height.
                        height = _textBlock.DesiredSize.Height;
                    } while (height > TwoLineLabel.LabelMaxHeight);
                }
                else
                {
                    // If the text and glyph both fit on one line, we insert a newline to force
                    // the glyph to a position below the label.
                    if (_path.Visibility == Visibility.Visible)
                    {
                        _run.Text += '\u0085';
                        _textBlock.Measure(infinity);
                    }
                }
            }

            return _textBlock.DesiredSize;
        }

        /// <summary>
        ///   Overrides arrange to simply arrange the child TextBlock.
        /// </summary>
        /// <param name="arrangeSize">The available size for the control to disply itself.</param>
        /// <returns>The arranged size.</returns>
        protected sealed override Size ArrangeOverride(Size arrangeSize)
        {
            _textBlock.Arrange(new Rect(arrangeSize));
            return arrangeSize;
        }

        /// <summary>
        ///   Gets the visual child of this element.
        /// </summary>
        /// <param name="index">The visual child index.</param>
        /// <returns>Returns the visual child of this element.</returns>
        protected override Visual GetVisualChild(int index)
        {
            if (index != 0)
            {
                // TODO : add to exception string table.
                throw new ArgumentOutOfRangeException("index", index, "Visual out of range");
            }

            return _textBlock;
        }

        #endregion

        #region Private Methods
        
        /// <summary>
        ///   Creates the textblock internal to the TwoLineLabel implementation and
        ///   binds the appropriate properties into the children of the element.
        /// </summary>
        private void CreateTextBlock()
        {
            Debug.Assert(_textBlock == null, "Internal TextBlock has already been created.");

            _run = new Run(Text);

            _path = new Path();
            _path.Margin = new Thickness(3.0);
            _path.VerticalAlignment = VerticalAlignment.Center;
            _path.SnapsToDevicePixels = true;

            Binding dataBinding = new Binding("Data");
            dataBinding.Source = this;
            _path.SetBinding(Path.DataProperty, dataBinding);

            Binding fillBinding = new Binding("GlyphFill");
            fillBinding.Source = this;
            _path.SetBinding(Path.FillProperty, fillBinding);
            
            _textBlock = new TextBlock();
            Binding textWrappingBinding = new Binding("TextWrapping");
            textWrappingBinding.Source = this;
            _textBlock.SetBinding(TextBlock.TextWrappingProperty, textWrappingBinding);

            Binding textAlignmentBinding = new Binding("TextAlignment");
            textAlignmentBinding.Source = this;
            _textBlock.SetBinding(TextBlock.TextAlignmentProperty, textAlignmentBinding);

            Binding lineHeightBinding = new Binding("LineHeight");
            lineHeightBinding.Source = this;
            _textBlock.SetBinding(TextBlock.LineHeightProperty, lineHeightBinding);

            Binding lineStackingStrategyBinding = new Binding("LineStackingStrategy");
            lineStackingStrategyBinding.Source = this;
            _textBlock.SetBinding(TextBlock.LineStackingStrategyProperty, lineStackingStrategyBinding);

            _textBlock.Inlines.Add(_run);
            _textBlock.Inlines.Add(_path);

            this.AddVisualChild(_textBlock);
        }

        #endregion
    }
}